package com.sd365.gateway.authorization.service.impl;


import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONObject;
import com.sd365.common.constant.Global;
import com.sd365.common.util.BeanUtil;
import com.sd365.common.util.JwtTokenBuilder;
import com.sd365.common.util.TokenUtil;
import com.sd365.gateway.authorization.constant.BusinessResultConsts;
import com.sd365.gateway.authorization.dao.mapper.ResourceMapper;
import com.sd365.gateway.authorization.dao.mapper.RoleResourceMapper;
import com.sd365.permission.centre.entity.RoleResource;
import com.sd365.gateway.authorization.service.AuthorizationService;
import com.sd365.gateway.authorization.service.RenewTokenService;
import com.sd365.gateway.authorization.service.remote.UserService;
import com.sd365.permission.centre.entity.Resource;
import com.sd365.permission.centre.pojo.vo.ResourceVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


import static com.sd365.common.util.TokenUtil.parseTokenPayload;
import static com.sd365.common.util.TokenUtil.parseToken;
import static java.util.regex.Pattern.compile;
/**
 * @Class AuthorizationServiceImpl
 * @Description
 * 增加对于token签名的认证
 * 1 从架构上重构了认证service 将 token续约分离出去定义在 RenewTokenService
 * 2 依据用户中心重构的 redis 以roleId 为key 构建对应的资源在hashmap的逻辑，重构了
 * 鉴权的角色对应的资源取得以及判定
 * @Author hyl
 * @Date 2023-04-26  22:14
 * @version 1.0.0
 */
@Slf4j
@RefreshScope
@Service
public class AuthorizationServiceImpl implements AuthorizationService {
    /**
     * Jwt Token签名秘钥，增加对于token签名的判断
     */
    @Value("${app.secret.token.key}")
    private String secret="$6$bosssoft$";
    /**
     * 用于token签名验证
     */
    @Autowired
    private JwtTokenBuilder jwtTokenBuilder;



    /**
     * 注意配置文件增加配置 调整该配置到 app。secret下
     */
    @Value("${app.secret.hasOpenAuthorization}")
    private Boolean hasOpenAuthorization;

    /**
     *  角色id 作为标示hashmap的key
     */
//    private static final String HASH_ROLE_ID_KEY="uc:cache:hash:role:id:";
    /**
     *  resource id作为缓存key
     */
//    private static final String RESOURCE_ID_KEY="uc:cache:resource:id:";

    /**
     * 匹配请求URL的正则表达式
     */
    private static final String AUTHOR_REQUEST_URL_EXPR="^https?:\\/\\/(?:[0-9a-zA-Z:\\.]*)([^\\?]*)";

    /**
     * 如果redis没取到则使用 该mapper取数据库
     */
    @Autowired
    private ResourceMapper resourceMapper;
    /**
     *  abel.zhan 增加 用于找到角色对应的资源列表
     */
    @Autowired
    private RoleResourceMapper roleResourceMapper;

    /**
     * 该redisTemplate 支持json序列化
     */
    @javax.annotation.Resource(name = "roleResourceRedisTemplate")
    private RedisTemplate<String,Object> redisTemplate;
    /**
     * 引入续约业务类做token状态判断
     */
    @Autowired
    private RenewTokenService renewTokenService;
    /**
     *   调用用户中心的UserApi的 getUserResourceVO接口使用
     *   此为FeignClient对象
     */
    private UserService userService;
    @Override
    public Boolean commonResource(String url)  {
        final Pattern compile = compile(AUTHOR_REQUEST_URL_EXPR);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            url = matcher.group(1);
        }
        List<Resource> commonResources = getCommonResource();

        if (!CollectionUtils.isEmpty(commonResources)) {
            for (Resource resource : commonResources) {
                if (!StringUtils.isEmpty(resource.getApi())) {
                    final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                    final Matcher matcherApi = pattern.matcher(url);
                    if (matcherApi.find()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * 获取通用资源，避免网关鉴权访问数据库而影响请求过程
     * abel.zhan 2023-05-30 用户中心增加了 通用资源缓存的代码，网关从hash中取得所有的通用资源
     * @author xiehl
     * @return  该资源是用户中心初始化存储的，用于鉴别么有纳入权限体系的资源
     */
    private List<Resource> getCommonResource() {
        List<Resource> commonResources =new ArrayList<>();
        // 先从redis中取，取不到再从数据库中取
        try {
            //status=2当做缓存的key
            Object o = redisTemplate.opsForValue().get(String.valueOf(2));
           Collection collection=redisTemplate.opsForHash().entries(Global.HASH_COMMON_RESOURCE_KEY).values();
            //获取全部的通用资源
            if(!CollectionUtil.isEmpty(collection)){
                commonResources = JSONObject.parseArray(String.valueOf(collection),Resource.class);
            }
        } catch (Exception e) {
            log.error("通用资源缓存获取失败");
            throw new  RuntimeException("通用资源缓存获取失败",e);
        }
        // 没取到只能去数据库取
        if(CollectionUtils.isEmpty(commonResources)){
            commonResources = resourceMapper.commonResource();
        }
        return commonResources;
    }

    /**
     * 鉴权主要逻辑
     * @param token  用户的token
     * @param api
     * @return
     */
    @Override
    public Integer roleAuthorization(String token, String api) {
        Assert.hasText(token,"token为空 roleAuthorization 参数异常");
        Assert.hasText(api,"api为空 roleAuthorization 参数异常");
        //解析token的 payload部分
        JSONObject joPayload = parseTokenPayload(token);
        // token 过期判断
        int tokenExpireResult=renewTokenService.expire(Long.parseLong(
                String.valueOf(joPayload.get("userId"))),token);
        if(BusinessResultConsts.TOKEN_EXPIRE_TURE==tokenExpireResult){
            return BusinessResultConsts.AUTHOR_RESULT_TOKEN_EXPIRE;
        }else if(BusinessResultConsts.TOKEN_EXPIRE_NEARLY==tokenExpireResult){
            return BusinessResultConsts.AUTHOR_RESULT_TOKEN_NEARLY;
        }

        // 如果token没有过期则根据角色获取资源列表
         List<Long> roleIdList=JSONObject.parseArray(String.valueOf(joPayload.get("roleIds")),Long.class);

         List<Resource> resourceList=getResourcesByRoleIds(roleIdList);
        //提取验证使用的url
        return matchRequestUrlWithResources(getMatchURL(api,AUTHOR_REQUEST_URL_EXPR),resourceList)
                ? BusinessResultConsts.AUTHOR_RESULT_TRUE
                : BusinessResultConsts.AUTHOR_RESULT_MATCH_RESOURCE_FALSE;
    }

    /**
     * 优先从redis中找，其次用户中心，最后数据库
     * @param roleIds 角色数组
     * @return
     */
    @Override
    public List<Resource> getResourcesByRoleIds(List<Long> roleIds) {
        List<Resource> resourceList=new ArrayList<>();
        // 先从缓存中找
        resourceList=getRoleResourceFromRedis(roleIds);
        if(!CollectionUtils.isEmpty(resourceList)){
            return resourceList;
        }
        // 再从用户中心找
        resourceList = getRoleResourceFromUserCenter(roleIds);
        if(!CollectionUtils.isEmpty(resourceList)){
            return resourceList;
        }
        // 最后从数据库找
        return getRoleResourceFromDB(roleIds);
//        resourceList=getRoleResourceFromDB(roleIds);
//        if(!CollectionUtils.isEmpty(resourceList)){
//            return resourceList;
//        }
//        return getRoleResourceFromUserCenter(roleIds);
    }

    /**
     * 优先从redis取得角色资源列表 资源的初始化在用户中心完成
     * @param roleIds  从token解析的角色id列表
     * @return  角色所对应的资源列表
     */
    private List<Resource> getRoleResourceFromRedis(List<Long> roleIds){
        Assert.notEmpty(roleIds,"roleIdsList不能为空");
        Assert.notEmpty(roleIds,"roleIds不能为空");
        /**
         * 存储多个角色的资源综合的 Map<Long, RoleResource>参考如下使用，keylong为role_resource表的行id
         *  我们要的数据是 RoleResource
         */

        Set< Map<Long, RoleResource> > mapSetRoleResource=new HashSet<>();
        // 返回一个角色对应的资源map并且添加到set，例如两个角色这个 这个set就2个元素
        roleIds.stream().forEach(id->{
            String hashKey=Global.HASH_ROLE_ID_KEY +id;
            Map roleResourceMap= redisTemplate.opsForHash().entries(hashKey);
            mapSetRoleResource.add(roleResourceMap);
        });
        /**
         * 找出每个角色对应的资源列表的具体的资源信息 使用set是为了避免重复
         * 首先遍历set中每个角色，取得map 然后遍历map将其中的resource取得增加到 resourceSet
         */
        Set<Resource> resourceSet=new HashSet<>();
        mapSetRoleResource.stream().forEach(map->{
            Collection collection=map.values();
         for(Object roleResource: map.values()){
             // 如果缓存和数据库都没取到该返回值为 new Resource
             String roleResourceJsonStr=JSONObject.toJSONString(roleResource);
             try{
                 RoleResource roleResourceTarget=JSONObject.parseObject(roleResourceJsonStr,RoleResource.class);
                 Resource resource= getResourceById( roleResourceTarget.getResourceId());
                 resourceSet.add(resource);
             }catch (Exception ex){
                 log.error("JSONObject.parseObject 发生错误:"+ex.getMessage(),ex);
             }


         }
        });
        return new ArrayList<Resource>(resourceSet);
    }

    /**
     *  从数据库获取角色资源列表
     * @param roleIds 角色列表 一个用户可能多个角色
     * @return 资源列表
     */
    private List<Resource> getRoleResourceFromDB(List<Long> roleIds){
        Example example=new Example(RoleResource.class);
        example.createCriteria().andIn("roleId",roleIds);
        // 查询角色所包含的资源id集合
        List<RoleResource> roleResourceList=roleResourceMapper.selectByExample(example);
        //将资源ｉｄ集合转为resource对象集合
        List<Resource> resourceList=new ArrayList<>();

        for(RoleResource roleResource : roleResourceList){
            Resource resource = getResourceById(roleResource.getResourceId());
            resourceList.add(resource);
        }
//        roleResourceList.stream().forEach(roleResource -> {
//            resourceList.add(getResourceById(roleResource.getResourceId()));
//        });
        return resourceList;
    }

    /**
     *  通过feign-client调用用户中心接口
     * @param roleIds
     * @return
     */
    private List<Resource> getRoleResourceFromUserCenter(List<Long> roleIds){
        List<ResourceVO> ResourceVOList=userService.getRoleResourceVO((Long[])roleIds.toArray());
        return  BeanUtil.copyList(ResourceVOList,Resource.class);
    }

    /**
     *  通过 resource_id 的值得找到 Resource信息
     * @param id  resource 记录的 id
     * @return resource对象
     */
    private Resource getResourceById(Long id){
        // 从 缓存获取 Resource 如果没有先从用户中心获取，最后再从数据库获取
        Object resource= redisTemplate.opsForValue()
                .get(Global.RESOURCE_ID_KEY+id);
        if(resource != null){
            return JSONObject.parseObject(JSONObject.toJSONString(resource),Resource.class);
        }

        // 取数据库 取用户中心接口
        resource=resourceMapper.selectByPrimaryKey(id);
        return resource == null ? null :new Resource();
    }

    @Override
    public boolean matchRequestUrlWithResources(String url, List<Resource> resources) {
        for (Resource resource : resources) {
            if (!StringUtils.isEmpty(resource.getApi())) {
                final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                final Matcher matcherApi = pattern.matcher(url);
                // 匹配成功
                if (matcherApi.find()) {
                    return Boolean.TRUE;
                }
            }
        }
        // 没有匹配到则返回false
        return Boolean.FALSE;
    }
    /**
     *  根据正则表达式匹配URL
     * @param url  URL
     * @param reg  正则表达式
     * @return  匹配后的URL 解析了请求的字符串
     */
    private String getMatchURL(String url,String reg){
       Assert.hasText(url,"url NOT NULL");
        Assert.hasText(reg,"url NOT NULL");
        String newUrl="";
        final Pattern compile = compile(reg);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            newUrl = matcher.group(1);
        }
        return newUrl;
    }

}
